Skip to content

Protocol conformance cache for generic types #82818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

DmT021
Copy link
Contributor

@DmT021 DmT021 commented Jul 5, 2025

This change adds a new type of cache (cache by type descriptor) to the protocol conformance lookup system. This optimization is beneficial for generic types, where the same conformance can be reused across different instantiations of the generic type.

Key changes:

  • Add a GetOrInsertManyScope class to ConcurrentReadableHashMap for performing multiple insertions under a single lock
  • Add type descriptor-based caching for protocol conformances
  • Add environment variables for controlling and debugging the conformance cache
  • Add tests to verify the behavior of the conformance cache
  • Fix for Runtime protocol conformance check unexpected first success #82889

The implementation is controlled by the SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR environment variable, which is enabled by default.

Here are some numbers for a synthetic example:

// slow_protocol_conformance.swift

import Foundation

func measureWithDispatchTime<T>(_ f: () -> T) -> (T, TimeInterval) {
  let start = DispatchTime.now()
  let result = f()
  let finish = DispatchTime.now()
  let delta = finish.uptimeNanoseconds - start.uptimeNanoseconds
  let time = Double(delta) / Double(NSEC_PER_SEC)
  return (result, time)
}

protocol Proto {}
struct S0<T>: Proto {}
struct S1<T>: Proto {}

func wrapEachInS(types: [Any.Type], depth: Int = 1) -> [Any.Type] {
  if depth == 0 {
    return types
  }
  func handleExistential<T>(_ type: T.Type) -> [Any.Type] {
    [
      S0<T>.self,
      S1<T>.self,
    ]
  }
  let wrapped = types.flatMap {
    _openExistential($0, do: handleExistential)
  }
  return wrapEachInS(types: wrapped, depth: depth - 1)
}

@_optimize(none)
public func blackHole(_: some Any) {}

let depth = if CommandLine.arguments.count == 2, 
  let value = Int(CommandLine.arguments[1]) {
  value
} else {
  20
}
let types = wrapEachInS(types: [Void.self], depth: depth)
func measure() -> TimeInterval {
  measureWithDispatchTime({
    for type in types {
      blackHole(type as? Proto.Type)
    }
  }).1
}
let n = 1 << depth
let firstMeasure = measure()
let secondMeasure = measure()
print("Time for \(n) types:")
print("  First = \(firstMeasure) s, or \(Int(firstMeasure / Double(n) * Double(NSEC_PER_SEC))) ns per type")
print("  Second = \(secondMeasure) s, or \(Int(secondMeasure / Double(n) * Double(NSEC_PER_SEC))) ns per type")
swiftc -c slow_protocol_conformance.swift -module-name main -O

On main:

/usr/bin/env \
  SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES=0 \
  DYLD_LIBRARY_PATH=/Users/dmt021/w/swift-project/build/Ninja+cmark-RelWithDebInfoAssert+llvm-RelWithDebInfo+swift-RelWithDebInfoAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/lib/swift/macosx \
  ./slow_protocol_conformance
Time for 1048576 types:
  First = 18.618541792 s, or 17756 ns per type
  Second = 0.059922125 s, or 57 ns per type
hyperfine '/usr/bin/env \
  SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES=0 \
  DYLD_LIBRARY_PATH=/Users/dmt021/w/swift-project/build/Ninja+cmark-RelWithDebInfoAssert+llvm-RelWithDebInfo+swift-RelWithDebInfoAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/lib/swift/macosx \
  ./slow_protocol_conformance'
Benchmark 1: /usr/bin/env \
  SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES=0 \
  DYLD_LIBRARY_PATH=/Users/dmt021/w/swift-project/build/Ninja+cmark-RelWithDebInfoAssert+llvm-RelWithDebInfo+swift-RelWithDebInfoAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/lib/swift/macosx \
  ./slow_protocol_conformance
  Time (mean ± σ):     20.356 s ±  0.264 s    [User: 20.032 s, System: 0.172 s]
  Range (min … max):   20.111 s … 20.950 s    10 runs

On the PR branch with the feature disabled:

/usr/bin/env \
  SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES=0 \
  SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR=0 \
  DYLD_LIBRARY_PATH=/Users/dmt021/w/swift-project/build/Ninja+cmark-RelWithDebInfoAssert+llvm-RelWithDebInfo+swift-RelWithDebInfoAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/lib/swift/macosx \
  ./slow_protocol_conformance
Time for 1048576 types:
  First = 18.620672959 s, or 17758 ns per type
  Second = 0.066553542 s, or 63 ns per type
hyperfine '/usr/bin/env \
  SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES=0 \
  SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR=0 \
  DYLD_LIBRARY_PATH=/Users/dmt021/w/swift-project/build/Ninja+cmark-RelWithDebInfoAssert+llvm-RelWithDebInfo+swift-RelWithDebInfoAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/lib/swift/macosx \
  ./slow_protocol_conformance'
Benchmark 1: /usr/bin/env \
  SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES=0 \
  SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR=0 \
  DYLD_LIBRARY_PATH=/Users/dmt021/w/swift-project/build/Ninja+cmark-RelWithDebInfoAssert+llvm-RelWithDebInfo+swift-RelWithDebInfoAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/lib/swift/macosx \
  ./slow_protocol_conformance
  Time (mean ± σ):     20.335 s ±  0.129 s    [User: 20.108 s, System: 0.173 s]
  Range (min … max):   20.169 s … 20.542 s    10 runs

On the PR branch with the feature enabled:

/usr/bin/env \
  SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES=0 \
  SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR=1 \
  DYLD_LIBRARY_PATH=/Users/dmt021/w/swift-project/build/Ninja+cmark-RelWithDebInfoAssert+llvm-RelWithDebInfo+swift-RelWithDebInfoAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/lib/swift/macosx \
  ./slow_protocol_conformance
Time for 1048576 types:
  First = 0.293545208 s, or 279 ns per type
  Second = 0.069242875 s, or 66 ns per type
hyperfine '/usr/bin/env \
  SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES=0 \
  SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR=1 \
  DYLD_LIBRARY_PATH=/Users/dmt021/w/swift-project/build/Ninja+cmark-RelWithDebInfoAssert+llvm-RelWithDebInfo+swift-RelWithDebInfoAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/lib/swift/macosx \
  ./slow_protocol_conformance'
Benchmark 1: /usr/bin/env \
  SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES=0 \
  SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR=1 \
  DYLD_LIBRARY_PATH=/Users/dmt021/w/swift-project/build/Ninja+cmark-RelWithDebInfoAssert+llvm-RelWithDebInfo+swift-RelWithDebInfoAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/lib/swift/macosx \
  ./slow_protocol_conformance
  Time (mean ± σ):      1.571 s ±  0.033 s    [User: 1.483 s, System: 0.078 s]
  Range (min … max):    1.514 s …  1.629 s    10 runs

@DmT021
Copy link
Contributor Author

DmT021 commented Jul 5, 2025

@swift-ci please test

@DmT021
Copy link
Contributor Author

DmT021 commented Jul 5, 2025

@swift-ci Please benchmark

@DmT021 DmT021 force-pushed the wp/conformance-cache-by-descriptor branch from 712d217 to 645d438 Compare July 6, 2025 03:38
@DmT021
Copy link
Contributor Author

DmT021 commented Jul 6, 2025

@swift-ci please test

@DmT021
Copy link
Contributor Author

DmT021 commented Jul 6, 2025

@swift-ci please smoke test

@DmT021
Copy link
Contributor Author

DmT021 commented Jul 6, 2025

@swift-ci please test macOS

@DmT021 DmT021 force-pushed the wp/conformance-cache-by-descriptor branch from 645d438 to e42a0e0 Compare July 9, 2025 00:31
@DmT021
Copy link
Contributor Author

DmT021 commented Jul 9, 2025

@swift-ci please test

@DmT021
Copy link
Contributor Author

DmT021 commented Jul 9, 2025

@swift-ci please smoke test macOS

@DmT021 DmT021 force-pushed the wp/conformance-cache-by-descriptor branch from e42a0e0 to 6b0c1ec Compare July 9, 2025 14:03
@DmT021
Copy link
Contributor Author

DmT021 commented Jul 9, 2025

@swift-ci please smoke test

@DmT021
Copy link
Contributor Author

DmT021 commented Jul 9, 2025

@swift-ci please smoke test Linux

@DmT021 DmT021 marked this pull request as ready for review July 10, 2025 01:44
Copy link
Contributor

@mikeash mikeash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! Nice idea. Hopefully the memory impact is small, should be if there are on average many generic types used for a given descriptor. I left a few very minor comments, but overall this looks really good.

This change adds a new type of cache (cache by type descriptor) to the protocol conformance lookup system. This optimization is beneficial for generic types, where the
same conformance can be reused across different instantiations of the generic type.

Key changes:
- Add a `GetOrInsertManyScope` class to `ConcurrentReadableHashMap` for performing
  multiple insertions under a single lock
- Add type descriptor-based caching for protocol conformances
- Add environment variables for controlling and debugging the conformance cache
- Add tests to verify the behavior of the conformance cache
- Fix for swiftlang#82889

The implementation is controlled by the `SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR`
environment variable, which is enabled by default.
@DmT021 DmT021 force-pushed the wp/conformance-cache-by-descriptor branch from 6b0c1ec to 7989dbe Compare July 11, 2025 01:54
@DmT021
Copy link
Contributor Author

DmT021 commented Jul 11, 2025

@swift-ci please smoke test

@DmT021
Copy link
Contributor Author

DmT021 commented Jul 11, 2025

@swift-ci please smoke test Windows

@DmT021 DmT021 requested a review from mikeash July 11, 2025 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants